package com.googlecode.cswish.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.log4j.Logger;
import org.apache.struts2.StrutsStatics;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.PageElement;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.SystemEnv;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.opensymphony.xwork2.ActionContext;

@Service
public class TagUtil {

	private static final Logger logger = Logger.getLogger(TagUtil.class);

	@Resource
	private Config config;
	
	@Resource
	private Functions functions;
	
	@Resource
	private BeanIntrospector beanIntrospector;

	@Resource
	private ITxFileService txFileService;
	
	@Resource
	private CssManager cssManager;
	
	@Resource
	private OgnlExUtil ognlExUtil;
	
	private Set<String> formItemStyles;
	
	private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

	private Map<String, String> versionPathCache = new ConcurrentHashMap<String, String>();
	
	@PostConstruct
	private void init() {
		formItemStyles = new HashSet<String>();
		formItemStyles.add("text");
		formItemStyles.add("number");
		formItemStyles.add("price");
		formItemStyles.add("password");
		formItemStyles.add("hidden");
		formItemStyles.add("file");
		formItemStyles.add("textarea");
		formItemStyles.add("combobox");
		formItemStyles.add("checkbox");
		formItemStyles.add("checkboxlist");
		formItemStyles.add("radio");
		formItemStyles.add("date");
		formItemStyles.add("treebox");
		formItemStyles.add("select");
		formItemStyles.add("modelselect");
		formItemStyles.add("enumselect");
		formItemStyles.add("autosearch");
		formItemStyles.add("dictselect");
		formItemStyles.add("span");
		formItemStyles.add("i18n");
	}

	public String css(String style, String property) {
		if (style == null || style.isEmpty()) {
			return "";
		}

		// remove ' '
		style = StringUtils.remove(style, ' ');

		// check rule: ";property:"
		int index = style.indexOf(property + ':');
		while (index > 0) {
			if (style.charAt(index - 1) == ';') {
				break;
			} else {
				index = style.indexOf(property + ':', index + 1);
			}
		}

		String value;
		if (index >= 0) {
			int end = style.indexOf(';', index + property.length() + 1);
			if (end == -1) {
				end = style.length();
			}

			value = style.substring(index, end);
		} else {
			value = "";
		}
		return value;
	}

	/**
	 * return 'val' if val is not empty return the css property only when the
	 * val is empty
	 * 
	 * @param val
	 * @param style
	 * @param property
	 * @return
	 */
	public String css(String val, String style, String property) {
		if (val == null || val.isEmpty()) {
			val = css(style, property);
		}
		return val;
	}
	
	public int css(int val, String style, String property) {
		if (val == 0) {
			String sval = css(style, property);
			val = toInteger(sval);
		}
		return val;
	}

	public int ncss(String val, String style, String property, int gap) {
		String sval = css(val, style, property);
		int len = sval.length();
		int nval = 0;
		if (len > 0) { // len == 0 means the length is unknown
			nval = toInteger(sval);
			nval += gap;
		}
		return nval;
	}
	
	public String mergeCssWidth(String style, int width) {
		// quick merge
		if (style.length() == 0) {
			if (width > 0) {
				return "width:" + width + "px";
			} else {
				return "";
			}
		}
		
		// check rule: ";property:"
		String property = "width";
		int index = style.indexOf(property + ':');
		while (index > 0) {
			if (style.charAt(index - 1) == ';') {
				break;
			} else {
				index = style.indexOf(property + ':', index + 1);
			}
		}
		int end = 0;
		if (index >= 0) {
			end = style.indexOf(';', index + property.length() + 1);
			if (end == -1) {
				end = style.length();
			}
		}
		
		if (width > 0) {
			// append width
			if (index >= 0) {
				// replace
				style = style.substring(0, index) + ";width:" + width + "px" + style.substring(end);
			} else {
				// add
				style += ";width:" + width + "px";
			}
		} else {
			// remove width
			if (index >= 0) {
				style = style.substring(0, index) + style.substring(end);
			}
		}
		return style;
	}

	private int toInteger(String sval) {
		int len = sval.length();
		StringBuilder sb = new StringBuilder(len);
		for (int i = 0; i < len; i++) {
			char c = sval.charAt(i);
			if (c >= '0' && c <= '9') {
				sb.append(c);
			}
		}
		if (sb.length() > 0) {
			return Integer.parseInt(sb.toString());
		} else {
			return 0;
		}
	}

	/*********************** table API - begin *******************************/
	@SuppressWarnings("unchecked")
	public int getEmptyColWidthCount(Map action, PageElements ps) {
		Stack<String> namePrefixStack = (Stack<String>) action.get(FrameConstant.NAME_PREFIX_STACK);
		String namePrefix = getNamePrefix(namePrefixStack);
		
		int count = 0;
		for (PageElement element : ps.getChildrenOfFirstLevel(namePrefix, true).values()) {
			if (element.getLength() == null || element.getLength() == 0) {
				count++;
			}
		}
		return count;
	}
	
	public Collection<String> getStatProperties(Map action, PageElements ps, Collection<String> coll, boolean autoStatNumber) {
		Stack<String> namePrefixStack = (Stack<String>) action.get(FrameConstant.NAME_PREFIX_STACK);
		String namePrefix = getNamePrefix(namePrefixStack);
		
		Collection<String> properties = new ArrayList<>();
		if (coll != null && coll.size() > 0) {
			if (namePrefix != null && namePrefix.length() > 0) {
				properties.addAll(coll);
			}
		}
		
		if (autoStatNumber) {
			Map<String, PageElement> children = ps.getChildrenOfFirstLevel(namePrefix, true);
			int prefixLen = namePrefix.length();
			for (PageElement pe : children.values()) {
				if ("number".equals(pe.getType()) && pe.getName().length() > prefixLen) {
					properties.add(pe.getName().substring(prefixLen));
				}
			}
		}
		return properties;
	}
	
	public String formatTableStat(Map action, PageElements ps, Collection coll, Collection<String> statProperties) {
		Stack<String> namePrefixStack = (Stack<String>) action.get(FrameConstant.NAME_PREFIX_STACK);
		String namePrefix = getNamePrefix(namePrefixStack);
		Map<String, PageElement> children = ps.getChildrenOfFirstLevel(namePrefix, true);
		int colCount = children.size();
		
		StringBuilder sb = new StringBuilder();
		if (coll == null || coll.size() == 0) {
			for (int i = 0; i < colCount; i++) {
				sb.append("<td></td>");
			}
		} else {
			statProperties = new HashSet<>(statProperties);
			
			// stat
			int prefixLen = namePrefix.length();
			for (PageElement pe : children.values()) {
				String name = pe.getName();
				if (name.length() > prefixLen && statProperties.contains(name.substring(prefixLen))) {
					double totalValue = 0f;
					String property = pe.getName().substring(prefixLen);
					for (Object item : coll) {
						Object value = ModelUtil.getProperty(item, property);
						if (value != null && value instanceof Number) {
							totalValue += ((Number)value).doubleValue();
						}
					}
					String strTotalValue =  ognlExUtil.convertValue(totalValue, String.class);
					sb.append("<td>").append(strTotalValue).append("</td>");
				} else {
					sb.append("<td></td>");
				}
			}
		}
		return sb.toString();
	}
	
	public int getColCount(Map action, PageElements ps) {
		Stack<String> namePrefixStack = (Stack<String>) action.get(FrameConstant.NAME_PREFIX_STACK);
		String namePrefix = getNamePrefix(namePrefixStack);
		
		int count = ps.getChildrenOfFirstLevel(namePrefix, true).size();
		return count;
	}
	
	public int getTotalColWidth(Map action, PageElements ps) {
		Stack<String> namePrefixStack = (Stack<String>) action.get(FrameConstant.NAME_PREFIX_STACK);
		String namePrefix = getNamePrefix(namePrefixStack);
		
		int width = 0;
		for (PageElement element : ps.getChildrenOfFirstLevel(namePrefix, true).values()) {
			if (element.getLength() != null) {
				width += element.getLength() + 6;		// +6 is the left padding(5) + border width(1)
			}
		}
		return width;
	}
	
	public int getCellWidth(int cfgWidth, int screenWidth, int totalWidth, int colCount, int emptyColWidthCount) {
		// 40 is the width of right scroll bar + container right bar, colCount
		// is all the border width
		int gap = screenWidth - totalWidth - 40 - colCount - colCount;
		if (gap > 10) {
			if (emptyColWidthCount == 0) {
				cfgWidth = cfgWidth + gap / colCount;
			} else {
				if (cfgWidth == 0) {
					cfgWidth = gap / emptyColWidthCount;
				}
			}
		}
		return cfgWidth;
	}

	public int getTableWidth(PageElements ps) {
		int len = 0;
		for (PageElement element : ps.getAllElements().values()) {
			if (element instanceof PageElements) {
				continue;
			}

			Integer length = element.getLength();
			if (length != null && length > 0) {
				len += length;
			} else {
				// get the default width
				// TODO: add more default value, read them from tagManager?
				if ("text".equals(element.getType())) {
					len += 160;
				} else {
					len += 0;
				}
			}
		}
		return len;
	}

	public String formatTableGridValue(String name, Object val, int maxTextLength) {
		String sVal = val.toString();
		int textLength = 0;
		int i = 0, len = sVal.length();
		for (; i < len && textLength <= maxTextLength; i++) {
			int c = sVal.charAt(i);
			if (c > 0 && c < 255) {
				textLength++;
			} else {
				textLength += 2;
			}
		}

		StringBuilder sb = new StringBuilder();
		sb.append("<div txtName='").append(name).append("' class='td'");
		if (maxTextLength > 0) {
			sb.append(" txtLen=").append(maxTextLength);
		}
		if (maxTextLength > 0 && textLength > maxTextLength) {
			sb.append(" title='").append(sVal).append('\'');
			sb.append(">");
			int c = sVal.charAt(i - 1);
			if (c > 0 && c < 255) {
				i -= 2;
			} else {
				i--;
			}
			sb.append(sVal.substring(0, i)).append("..");
		} else {
			sb.append(">");
			sb.append(sVal);
		}
		sb.append("</div>");
		return sb.toString();
	}
	/*************************** table API - end *****************************/
	
	public String getIncludeUrl(String url, boolean isMock) {
		int index = url.indexOf('/', 1);
		String urlPrefix;
		if (index == -1) {
			urlPrefix = url;
		} else {
			urlPrefix = url.substring(0, index + 1);
		}
		boolean isStaticUrl = config.getViewDir().equals(urlPrefix)
			|| config.getCssDir().equals(urlPrefix) || config.getImgDir().equals(urlPrefix);
		
		String newURL;
		if (isStaticUrl) {
			newURL = url;
		} else {
			if (isMock) {				
				// see TxFileService.read/write
				newURL = txFileService.getRealTxFilePath(url, false); 
				if (newURL.startsWith("/site/")) {
					String path = config.getRealWebRootPath() + newURL;
					if (!new File(path).exists()) {
						newURL = "";
					}
				}
			} else {
				newURL = config.getSitePath() + url;
			}
		}
		return newURL;
	}
	
	public String include(String url) {
		String content = "";
		if (url.startsWith("http://")) {
			HttpClient client = HttpClientBuilder.create().build();
			HttpGet request = new HttpGet(url);
			try {
				HttpResponse response = client.execute(request);
				content = IOUtils.toString(response.getEntity().getContent());
			} catch (Exception ex) {
				logger.error("Failed to read site:" + url, ex);
			}
		} else {
			String realPath = config.getRealWebRootPath();
			String filePath = realPath + url;
			try {
				content = FileUtils.readFileToString(new File(filePath), "UTF-8");
			} catch (Exception ex) {
				logger.error("Failed to read tag file:" + filePath, ex);
			}
		}
		return content;
	}

	private String cssFile(PageElements ps, String prefix, String name, boolean replaceHtmlBodyCss) {
		int index = name.lastIndexOf('/');
		String oldName = name;
		int nameBegin;
		boolean isHttpFile = false;
		if (name.startsWith("/static/")) {
			nameBegin = 7;
		} else {
			if (name.startsWith("http://") || name.startsWith("https://")) {
				isHttpFile = true;
				nameBegin = name.indexOf('/', 8);
			} else {
				nameBegin = 0;
			}
		}
		int pos = prefix.indexOf(':');
		String formatPrefix = pos > 0 ? prefix.substring(1, pos) : prefix.substring(1);
		// 'replaceHtmlBody' to avoid the css to affect the top page
		String newName = config.getSitePath() + name.substring(nameBegin, index + 1) 
				+ name.substring(index + 1, name.length() - 4) + '.' + formatPrefix
				+ '.' + (replaceHtmlBodyCss ? "0" : "1") + ".tmp.css";
		
		String rootPath = config.getRealWebRootPath();
		File oldFile;
		URL url = this.getClass().getResource(oldName);
		if (url != null) {
			oldFile = new File(url.getPath());
		} else {
			oldFile = isHttpFile ? new File(oldName) : new File(rootPath + oldName);
		}
		String newFullName = rootPath + newName;
		File newFile = new File(newFullName);
		long t;
		boolean foundConflictCss = true;		// TODO: find a way to check if there's confict css library
		boolean generateCssFile = foundConflictCss && (
					ps.isMock() || !newFile.exists()
					|| (t = newFile.lastModified()) < Long.valueOf(System.getProperty(SystemEnv.STARTUP_TIME))
					|| t < oldFile.lastModified());
		if (generateCssFile) {
			try {
				String oldContent;
				if (url != null) {
					InputStream input = null;
					try {
						input = this.getClass().getResourceAsStream(name);
						oldContent = IOUtils.toString(input, "UTF-8");
					} finally {
						IOUtils.closeQuietly(input);
					}
				} else {
					String oldFullName = null;
					File oldFullFile = null;
					if (ps.isMock()) {
						// load the content from temporary file
						oldFullName = txFileService.getRealTxFilePath(rootPath + name, false);
						oldFullFile = new File(oldFullName);
					}
					if (oldFullFile == null || !oldFullFile.exists()) {
						oldFullName = isHttpFile ? name : rootPath + name;
						oldFullFile = new File(oldFullName);
					}
					if (isHttpFile) {
						InputStream inputStream = new URL(name).openStream();
						oldContent = IOUtils.toString(inputStream);
						IOUtils.closeQuietly(inputStream);
					} else {
						if (oldFullFile.exists()) {
							oldContent = FileUtils.readFileToString(oldFullFile, "UTF-8");
						} else {
							oldContent = "";
						}
					}
				}
				String absoluteUrlPrefix = Functions.getBase() + oldName.substring(0, oldName.lastIndexOf('/') + 1);
				String newContent = cssManager.appendPrefix(oldContent, prefix, absoluteUrlPrefix, replaceHtmlBodyCss);
				FileUtils.writeStringToFile(newFile, newContent, "UTF-8");
			} catch (Exception ex) {
				logger.error("Failed to read tag file:" + name, ex);
				newName = name;
			}
		}
		return newName;
	}

	public String getCssPath(PageElements ps, String prefix, String src) {
		return getCssPath(ps, prefix, src, true);
	}
	
	public String getCssPath(PageElements ps, String prefix, String src, boolean enableReplaceHtmlBodyCss) {
		if (src.isEmpty()) {
			return src;
		}

		// the possible path:
		// 1. src
		// 2. site/xxx/src
		boolean isCommonResource = src.startsWith("/static/");
		// TODO: remove this function
		boolean isClassResource = !isCommonResource &&
					Thread.currentThread().getContextClassLoader().getResource(src.substring(1)) != null;
		boolean isMock = ps.isMock();
		if (isMock) {
			src = txFileService.getRealTxFilePath(src, isCommonResource || isClassResource);
			
			// avoid the inner css affect the whole page
			if (StringUtils.isEmpty(prefix)) {
				prefix = "#pageCtn";
			}
		} else {
			// see page.update.js, showCodeEditor
			if (!isCommonResource && !isClassResource) {
				if (!src.startsWith("http://") && !src.startsWith("https://")) {
					src = config.getSitePath() + src;
				}
			}
		}

		String cssSrc = src;
		Collection<String> categories = ps.safeContext().getCssCategories();
		boolean enablePrefix = categories.size() > 1;		// has more category excluding itself
		if (isMock || enablePrefix && prefix != null && !prefix.isEmpty()) {
			cssSrc = cssFile(ps, prefix, cssSrc, enableReplaceHtmlBodyCss);	
		}
		
		return getVersionPathWithBase(cssSrc);
	}
	
	public String getStaticPath(String src) {
		return Functions.getBase() + src;
	}

	public String getScriptPath(String src, boolean isMock) {
		if (src.isEmpty() || src.startsWith("http://") || src.startsWith("https://")) {
			return src;
		}
		
		boolean isCommonResource = src.startsWith("/static/");
		boolean isClassResource = !isCommonResource 
					&& Thread.currentThread().getContextClassLoader().getResource(src.substring(1)) != null;
		if (isMock) {
			src = txFileService.getRealTxFilePath(src, isCommonResource || isClassResource); 
		} else {
			// see page.update.js, showCodeEditor
			if (!isCommonResource && !isClassResource) {
				src = config.getSitePath() + src;
			}
		}
		
		return getVersionPathWithBase(src);
	}

	public String getImagePath(String src, String defaultSrc) {
		if (src.length() == 0) {
			return getImagePath(defaultSrc);
		} else {
			return getImagePath(src);
		}
	}
	
	public String getImagePath(String src) {
		if (src.startsWith("http://") || src.startsWith("https://") || src.length() == 0) {
			return src;
		}

		// FIXME: fix the legacy code issue, remove it in future
		if (src.startsWith("/images/default/")) {
			src = "/static/espirit" + src;
		}
		
		boolean isCommonResource = src.startsWith("/static/");
		boolean isClassResource = !isCommonResource 
					&& Thread.currentThread().getContextClassLoader().getResource(src.substring(1)) != null;
		if (isCommonResource || isClassResource) {
			return Functions.getBase() + src;
		} else {
			return Functions.getBase() + config.getSitePath() + src;
		}
	}
	
	public String formatImagePath(String src, String replacement) {
		String replaced = StringUtils.replace(src, "{0}", replacement);
		return getImagePath(replaced);
	}
	
	public String getImagePathPrefix(String src) {
		if (src.startsWith("http://") || src.startsWith("https://")) {
			return "";
		}
		
		if (src.length() == 0 || src.startsWith("/static/")) {
			return Functions.getBase();
		} else {
			String prefix = Functions.getBase() + config.getSitePath();
			return prefix;
		}
	}

	/**
	 * Get the display name (label name) of the combobox
	 * 
	 * @return
	 */
	public String getComboboxDisplayName(String name, String labelName, String property, String labelProperty,
			boolean enableDynamicName) {
		String newLabelName;
		// enableDynamicName, FIXME: use different fields to populate the
		// property value and label property value?
		if (property.equals(labelProperty) || labelName.length() > 0 || !enableDynamicName) {
			if (labelName.endsWith(name)) {
				newLabelName = "";			// avoid the duplicated name
			} else {
				newLabelName = labelName;
			}
		} else {
			// build the label name according to the property & label property
			// FIXME: name must contain the 'property'? how to process if the
			// name hasn't relationship with property? append the property
			// directly?
			if (name.endsWith('.' + property)) {
				newLabelName = name.substring(0, name.length() - property.length()) + labelProperty;
			} else {
				newLabelName = name + '.' + labelProperty;
			}
		}

		return newLabelName;
	}

	/**
	 * Get the display value (label value) of the combobox
	 * 
	 * @return
	 */
	public Object getComboboxDisplayValue(Object action, String name, String labelName, String property,
			String labelProperty, boolean enableFreeText, Object value, Integer model, Object item, String namePrefix) {
		Object labelValue;
		if (property.equals(labelProperty)) {
			labelValue = getValue(action, name, model, item, namePrefix);
		} else {
			if (enableFreeText && !isEmpty(value)) {
				labelValue = value;
			} else {
				if (labelName.length() == 0) { // create the label name temporarily?
					if (name.endsWith('.' + property)) {
						labelName = name.substring(0, name.length() - property.length()) + labelProperty;
					} else {
						labelName = name + '.' + labelProperty;
					}
				}
				labelValue = getValue(action, labelName, model, item, namePrefix);
			}
		}
		return labelValue;
	}
	
	public String getComboboxLabelProperty(String name, String property, String labelProperty) {
		if (property.equals(labelProperty)) {
			return labelProperty;
		} else {
			String labelName;
			if (name.endsWith('.' + property)) {
				labelName = name.substring(0, name.length() - property.length()) + labelProperty;
			} else {
				labelName = name + '.' + labelProperty;
			}
			return labelName;
		}
	}
	
	// macro i18n begin
	public String getI18nName(String name) {
		if (name.endsWith(".id")) {
			return name;
		} else {
			return name + ".id";
		}
	}
	
	public String getI18nLabelName(String name) {
		if (name.endsWith(".id")) {
			String labelName = name.substring(0, name.length() - 2) + "name";
			return labelName;
		} else {
			return name + ".name";
		}
	}
	// macro i18n end
	
	private boolean isEmpty(Object val) {
		if (val == null) {
			return true;
		}

		if (val instanceof String) {
			return ((String) val).length() == 0;
		} else {
			return false;
		}
	}

	public Object getValue(Object action, String name, Integer model, Object item, String namePrefix) {
		Object value;
		String fullName = (namePrefix == null || namePrefix.length() == 0 || name.length() > 0 && name.charAt(0) == ':')
				? name : namePrefix + name;
		if (model != null && model > 0) {
			if (model == 6) { // search template
				value = functions.val(action, fullName, fullName);
			} else { // iterator pageinfo, datagrid, collection
				value = functions.val4ValidName(item, name, fullName);
			}
		} else {
			value = functions.val(action, fullName, fullName);
		}
		return value == null ? "" : value;
	}

	public Object getPageElement(PageElements ps, String name, Integer model, String namePrefix) {
		PageElement pageElement = null;
		if (ps != null) {
			if (name.length() > 0) {
				String fullName = (namePrefix == null || namePrefix.length() == 0) ? name : namePrefix + name;
				pageElement = ps.getElement(fullName, false);
			} else {
				pageElement = ps;
			}
		}
		if (pageElement == null) {
			return false;
		} else {
			return pageElement;
		}
	}

	public String formatName(String name) {
		if (name.length() > 0 && name.charAt(0) == ':') {
			int index = name.indexOf(':', 1);
			if (index > 0) {
				name = name.substring(index);
			}
		}
		return name;
	}

	// ///////////////////////////////////////////// utility class for tag
	// /////////////////////////////////
	public Object getLabelValue(Collection collection, Object value) {
		return getLabelValue(collection, "id", "name", value);
	}
	
	public Object getLabelValue(Collection collection, String propertyName, String labelPropertyName,
			Object value) {
		String str = functions.s(value, propertyName);
		if (str.length() > 0) {
			Object item = functions.indexof(collection, propertyName, str);
			if (item != null) {
				return functions.val(item, labelPropertyName);
			} else {
				// Can't find the label by the key, use the key as the label
				return str;
			}
		}
		return "";
	}

	public boolean isFormItemStyle(String type) {
		return formItemStyles.contains(type);
	}

	public String formatTitle(String val) {
		// TODO: only keep the text if it's html?
		return StringUtils.replace(val, "\"", "&quot;");
	}

	/**
	 * Skip the first '.'
	 * 
	 * @param name
	 * @return
	 */
	public String formatFileName(String name) {
		String formatName = null;
		name = name.substring(name.lastIndexOf('/') + 1);
		int index1 = name.indexOf('.');
		if (index1 > 0) {
			int index2 = name.indexOf('.', index1 + 1);

			if (index2 > 0) {
				formatName = name.substring(index1 + 1);
			}
		}
		if (formatName == null) {
			formatName = name.substring(name.lastIndexOf('/') + 1);
		}
		return formatName;
	}

	private String getVersionPathWithBase(String path) {
		boolean enableVersion = false;
		if (!enableVersion || config.isDevMode()) {
			return Functions.getBase() + path;
		}
		
		String versionPath = versionPathCache.get(path);
		
		// TODO: compare the modified date?
		
		if (versionPath == null) {
			String realPath = config.getRealWebRootPath() + path;
			try {
				String content = FileUtils.readFileToString(new File(realPath));
				String version = encodeByMD5(content);
				versionPath = Functions.getBase() + path + "?v=" + version;
			} catch (IOException ex) {
				versionPath = Functions.getBase() + path;
				logger.error("Failed to read file content:" + path, ex);
			}
			versionPathCache.put(path, versionPath);
		}
		return versionPath;
	}
	
	private static String encodeByMD5(String str) {
		if (str == null) {
			return null;
		}
		try {
			MessageDigest messageDigest = MessageDigest.getInstance("MD5");
			messageDigest.update(str.getBytes());
			return getFormattedText(messageDigest.digest());
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	private static String getFormattedText(byte[] bytes) {                                        
	    int len = bytes.length;                                                                   
	    StringBuilder buf = new StringBuilder(len * 2);                                           
	    // 把密文转换成十六进制的字符串形式                                                       
	    for (int j = 0; j < len; j++) {          buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);  
	        buf.append(HEX_DIGITS[bytes[j] & 0x0f]);                                              
	    }                                                                                         
	    return buf.toString();                                                                    
	}
	
	// see PageElement.getShortName
	public String getShortName(String name) {
		if (name.startsWith("_gen_")) {
			// exmaple: _gen_xxx_idyyy ==> yyy
			int endIndex = name.lastIndexOf('_');
			if (endIndex > 0) {
				int beginIndex = name.lastIndexOf('_', endIndex - 1);
				if (beginIndex > 0) {
					name = name.substring(beginIndex + 1, endIndex);
				} else {
					name = "";
				}
			} else {
				name = "";
			}
		}
		return name;
	}
	
	public String getCid(String name, String tagName) {
		boolean isFullName = name.startsWith("_gen_");
		if (isFullName) {
			return name;
		} else {
			// TODO: correct? use tagName + pageIndex?
			String actionName = (String)ActionContext.getContext().getValueStack()
				.getContext().get(FrameConstant.STACK_ACTIONNAME);
			if (actionName == null) {
				actionName = tagName;
			}
			return buildCidName(tagName, actionName);
		}
	}
	
	public String buildCidName(String tagName, String actionName) {
		int end = actionName.lastIndexOf('.');
		int begin = 0;
		if (end > 0) {
			begin = actionName.lastIndexOf('.', end - 1) + 1;
		}
		end = actionName.length() - 6;		// skip Action
		StringBuilder sb = new StringBuilder();
		sb.append('_').append(tagName).append('_');
		for (int i = begin; i < end; i++) {
			char c = actionName.charAt(i);
			if (c == '.') {
				sb.append('_');
			} else if (Character.isUpperCase(c)) {
				char prevChar = sb.charAt(sb.length() - 1);
				if (prevChar == '_') {
					sb.append(Character.toLowerCase(c));
				} else {
					sb.append(c);
				}
			} else {
				sb.append(c);
			}
		}
		return sb.toString();
	}
	
	public String pushNamePrefix(Map action, String namePrefix) {
		Stack<String> namePrefixStack = (Stack<String>) action.get(FrameConstant.NAME_PREFIX_STACK);
		if (namePrefix != null && namePrefix.length() > 0) {
			if (namePrefixStack == null) {
				namePrefixStack = new Stack();
				action.put(FrameConstant.NAME_PREFIX_STACK, namePrefixStack);
			}
			namePrefixStack.push(namePrefix);
		}
		return getNamePrefix(namePrefixStack);
	}
	
	private String getNamePrefix(Stack<String> stack) {
		StringBuilder sb = new StringBuilder();
		if (stack != null) {
			for (String prefix : stack) {
				if (sb.length() == 0) {
					sb.append(prefix);
				} else {
					if ("model".equals(prefix) || "execute".equals(prefix)) {
						continue;
					} else {
						sb.append(prefix);
					}
				}
				sb.append('.');
			}
		}
		return sb.toString();
	}
	
	public String popNamePrefix(Map action, String namePrefix) {
		Stack<String> namePrefixStack = (Stack<String>) action.get(FrameConstant.NAME_PREFIX_STACK);
		if (namePrefix != null && namePrefix.length() > 0) {
			namePrefixStack.pop();
		}
		return getNamePrefix(namePrefixStack);
	}
	
	public String formatLink(String url, PageElements ps, boolean proxyCrossSiteUrl) {
		// TODO: check & remove the base path automatically?
		String base = Functions.getBase();
		if (base.length() > 0 && url.startsWith(base)) {
			url = url.substring(Functions.getBase().length());
		}
		
		String formatLink;
		if (url != null) {
			if (url.startsWith("http://") || url.startsWith("https://") 
					|| url.startsWith("javascript:") || url.startsWith("#")) {
				formatLink = url;
			} else if (ps.isMock()) {
				formatLink = functions.link(url);
			} else {
				
				SafeManager safeManager = SafeManager.getInstance();
				ActionContext context = ActionContext.getContext();
				if (context != null && safeManager.isEnablePermissionCheck()) {
					HttpServletRequest request = (HttpServletRequest) context.get(StrutsStatics.HTTP_REQUEST);
					UserInfo userInfo = safeManager.getLoginUser(request);
					
					// format the check page
					String checkPage = functions.linkNoBase(url, true);
					int index = checkPage.indexOf('?');
					if (index > 0) {
						checkPage = checkPage.substring(0, index);
					}
					
					boolean pass = safeManager.checkPage(userInfo == null ? null : userInfo.getLoginId(), checkPage);
					if (pass) {
						formatLink = functions.link(url);
					} else {
						formatLink = "";
					}
				} else {
					formatLink = functions.link(url);
				}
			}
		} else {
			formatLink = "";
		}
		
		return StringUtils.replace(formatLink, "\"", "\\\"");
	}
	
	// macro pageinfo
	public String getPageInfoClickLink(String base, Collection<String> links, String joinStr) {
		StringBuilder sb = new StringBuilder();
		
		boolean first = true;
		for (String link : links) {
			// joinStr
			if (first) {
				first = false;
			} else {
				sb.append(joinStr);
			}
			
			// base + link
			if (link != null && link.length() > 0) {
				sb.append(base);
				sb.append(link);
			}
		}
		
		return sb.toString();
	}
	
	// ftl path
	public String getGenFtl(String path) {
		return getGenFtl(path, false);
	}
	
	public String getGenFtl(String path, boolean withSitePath) {
		String ftl;
		if (functions.hasPage(path)) {
			StringBuilder sb = new StringBuilder();
			if (withSitePath) {
				sb.append("/site/").append(ContextHolder.get().getSiteName());
			}
			sb.append(config.getViewDir4GenCode());
			if (path.length() > 0 && path.charAt(0) == '/') {
				sb.append(path.substring(1));
			} else {
				sb.append(path);
			}
			if (!path.endsWith(".ftl")) {
				sb.append(".ftl");
			}
			ftl = sb.toString();
		} else {
			ftl = "/view/page.undefined.ftl";
		}
		return ftl;
	}
}